iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI Engineering

一天一個 PR,讓我告訴你嵌入式開發的殘酷:如何為 AI Agent 建立可信的工程閉環系列 第 14

Day 14 - 同一個 API 在 Workbook 有 20 列:李逵?李鬼?傻傻分不清楚

  • 分享至 

  • xImage
  •  

系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。

我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。

本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。

Today’s Change

  • Change Ref: PR #148-feat(audit): pass12 source.row tiebreak + Cf 字元淨化 + init hygiene lint

  • Issue: Pre-audit 的 85 個 Case 中,有 27 案因 workbook_row_ambiguous 被 Block;同一組 (object, api) 在 Workbook 可能一次對到 14~20 列,Audit 根本不知道該拿哪一列當答案。

  • Root Cause: Workbook 的實際粒度是「Method × 回傳欄位」,Audit 卻只用 (object, api) 當 Alignment Key,把真正有差異的維度壓扁了;另外還有肉眼看不見的 Unicode Format Character 混在資料裡搞事。

  • Solution: (object, api) 一對多時,只允許 Case YAML 的 source.row 在 Candidate Rows 裡做 Tiebreak,不升格成 Lookup Key;同步加入 row_resolution、Unicode Cf 淨化與 Advisory workbook_lint.json

  • Evidence: 全套測試 2026 passedpolicy_check 0 fail;原本 27 個 workbook_row_ambiguous 實機重跑後歸零,27/27 都記錄為 source_row_tiebreak,並繼續進入 needs_pass3


昨天才把 Resume 做好,今天存下來的東西就開始找麻煩

上一篇,我們終於讓長時間 Audit 可以中斷續跑。

Case 跑完會進 Bucket,Worklist 知道哪些做過,--resume 不會每次都把前面幾個小時的人生重新活一次。Summary 還開始記:

Case
Workbook Row
Expected
Verdict
Bucket

看起來有序、流暢、舒服、爽。

一套自動化系統終於開始有「昨天做過的事,今天還記得」這種人類基本能力。

然後 Pre-audit 一跑,85 個 Case 分成:

confirmed       8
block          27
needs_pass3    50

27 個 Block,Reason 全部一樣:

workbook_row_ambiguous

What The Fu... Form!!!

一定又是哪個 Excel Cell 寫歪了。

查著查著~欸,好像不是耶。

Audit 找得到資料,而且找得非常勤勞。

一找就是二十列。

例如 getRadioStats()

row263
row264
row265
...
row276
row454
...

老Go看了一眼。

「你不是說上一篇已經把 Workbook Row 記下來了?」

「有啊。」

「那是哪一列?」

「……這二十列其中一列,One of them。」

很好。

昨天努力留下的證據,今天正式升級成:

我有存。

存哪個?阿災。


最直覺的想法:重複列刪一刪不就好了?

同一個:

WiFi.Radio.{i}.
getRadioStats()

竟然出現二十列。

這不是 Duplicate 是什麼?

去重啊。

Excel 最擅長的就是製造重複資料,我最擅長的就是嫌 Excel。

Perfect match。

但真的把內容攤開:

row263  getRadioStats()  BroadcastPacketsReceived
row265  getRadioStats()  BytesReceived
row276  getRadioStats()  UnicastPacketsSent
row454  getRadioStats()  FailedRetransCount
...

靠。

它們根本不是 Duplicate。

同一支 getRadioStats() 會回很多 Field,Workbook 的設計就是一個 Field 一列。Object 一樣、API 一樣,真正不同的東西藏在另一個欄位。

所以如果我很帥地寫個 Script:

duplicate row → remove

然後一鍵清掉十九列。

那不叫 Data Cleanup。

那叫滅門。

Excel 沒有重複。

是我們眼睛只看兩欄,硬把二十個不同的人叫成同一個名字。

Target


原來不是資料有問題,是我們自己把資料拍扁了

pass12 原本找 Workbook Row 的方法很單純:

normalize(object)
normalize(api)
        ↓
key = (object, api)
        ↓
Workbook Index

一列就拿。

零列是 workbook_row_missing

兩列以上不猜,直接 workbook_row_ambiguous

這邏輯其實沒毛病。

我甚至很喜歡它「不知道就 Block」的個性。

至少不像某些 Agent,資料不夠也可以先給你兩千字 Root Cause Analysis,最後補一句:

Based on the available evidence, this is highly likely.

Highly likely 個鬼。

問題是 Workbook 真正的資料粒度比較像:

(object, api, return-field)

Audit 卻只留下:

(object, api)

BroadcastPacketsReceivedBytesReceivedFailedRetransCount……

二十個不同 Test Item,全被拍成同一個 getRadioStats()

然後我們再很震驚地問:

為什麼一個 Key 對二十列?

因為你自己把名字削掉了啊,大哥。

Parser 沒瘋。

是我們問問題時只講了一半。

這也是這一篇真正的問題。

不是 Workbook 有 Duplicate。

Workbook 跟 Audit 對同一筆資料的粒度根本不一樣。

二十個李逵站成一排。

Audit 看著他們:

你們誰是李逵?

全部舉手。


答案早在 Case YAML 裡,但沒人關心它

更討厭的是,每一個 D### Case 本來就有:

source:
  row: 263

D263 指 Row 263,D265 指 Row 265,D276 指 Row 276。

也就是說:

答案一直都在。

只是 Audit Alignment 做久了,老了、油了,完全把它當空氣。

A Component 很認真地說:

我的設計只相信 (object, api)

旁邊 Metadata:

我知道答案耶。

A Component:

閉嘴,你不是 Authority。

原則沒有錯。

但原則用過頭,就會從 Architecture 變固執。

於是第一個想法自然變成:

那直接用 source.row 不就好了?

Case 說第 263 列,我們就拿第 263 列。

一翻兩瞪眼。

老Go又來了。

「那 Workbook 中間插一列之後,263 還是原本那個 263?」

「……」

好了。

下一題。

直接把 source.row 當 Lookup Key,今天當然很爽。

至於半年後會不會多寫十篇,那是半年後的我。

但 Workbook 是活的。有人插 Row、有人換版、有人整理 Template。263 不會因為內容漂移就自己羞愧地變成 UNKNOWN

它只會非常堅定地繼續當 263。

所以 PR #148 沒讓 source.row 登基。

它只給它一個比較小的工作:

(object, api)
先找 Candidate

只有一列
→ unique

有多列
→ source.row 只在 Candidate 裡 Tiebreak

對不上
→ 繼續 Block

source.row 說的不是:

「第 263 列是真理。」

而是:

「你已經抓到這二十個嫌疑犯,我可以告訴你原始 Case 指的是哪一個。」

有投票權。

沒有登基。

Metadata 可以輔助 Identity。

不能因為正好救了你一次,就現場加冕成皇帝。

Target


Excel 說:只有 Row Ambiguous?想得美

Row Tiebreak 差不多有方向後,對抗性檢查又翻出另一個東西。

Workbook 有幾列 AffiliatedSTA,肉眼看完全正常,但就是對不上。

我盯了半天。

沒有多一個點。

沒有少一個括號。

大小寫也一樣。

Agent 把 Codepoint 拆出來。

裡面躲了一個:

U+200B ZERO WIDTH SPACE

零寬空白。

這名字取得真好。

它的主要功能就是讓你看不到它。

對人眼:

AffiliatedSTA.{i}
AffiliatedSTA.​{i}

「一樣啊?」

對 Python:

不一樣,謝謝。

很好。

前面二十列是東西太多。

這次是東西根本看不到。

Excel 非常公平,各種方向都照顧到了。

所以 normalize_object()normalize_api() 也開始清 Unicode Cf Format Character。

但 Raw Workbook Value 還是留著。

比對時可以忽略垃圾。

稽核時還是要知道垃圾原本在哪。

不然 System 很貼心地幫你擦掉,再宣布:

Data quality looks good.

那不是 Normalization。

那比較像毀屍滅跡。

既然都看到這裡了,這次 audit init 也順手產:

workbook_lint.json

先列 Duplicate Keys、Invisible Characters、Empty Verdicts。

注意,它只是 Advisory

因為剛才那二十列已經證明:Duplicate Key 不一定有罪,可能只是 Workbook 的資料模型比你的 Index 更細。

Lint 只負責先說:

這邊怪怪的,你最好知道。

至於是不是 Bug,後面再判。

而Lint ,就是hygiene lint,這次PR的主角擔當,扛屎擦尿。

另外 audit init 原本 stdout 只吐 RID,外部 Script 可以直接:

RID=$(testpilot audit init ...)

所以 Lint Summary 改走 stderr。

不然「只是多印一行」就能讓舊 Script 當場往生。

這句話在自動化工具裡的地位,大概跟恐怖片裡的:

我出去看一下。

差不多。

Target


昨天才叫 Resume 不要重跑,今天請它先閉嘴

修完之後,當然要把那 27 個原本 Block 的 Case 重新跑一遍。

然後昨天才做好的 --resume 馬上出來表演什麼叫忠於職守。

我:

幫我重跑這 27 案。

Resume:

已經 Bucketed,Skip。

我:

我就是要重驗。

Resume:

但你昨天不是說做過的不要重跑?

我:

……

靠。

完全符合 Spec。

一點 Bug 都沒有。

問題是今天我要的不是 Resume。

我要的是 Re-evaluate。

所以這次重驗原本 Block 的 Case,反而不能帶 **--resume**

昨天不重跑是正確。

今天不重跑就是錯。

Tool 沒壞。

是你今天叫它做的事變了。


27 個 Block 歸零,結果一個都沒有順便變綠

實機重跑後:

workbook_row_ambiguous
27 → 0

27/27 的 pass1_baseline.json 都留下:

row_resolution = source_row_tiebreak

全套測試:

2026 passed

policy_check

0 fail

也就是原本被 (object, api) 壓在一起的 Case,現在能 deterministic 地認回各自的 Workbook Candidate Row。

實機途中仍然有既有環境不穩定,但那不是今天要證明的東西。

今天只問一題:

原本 ambiguous 的 Workbook Row,現在到底認不認得回來?

答案是認得。

別順手替它寫成:

整套 Wi-Fi Audit 從此天下太平。

沒有。

先不要。

真正有意思的是下一個結果:

27 / 27 → needs_pass3

沒有一案因為 Row 對準了,就自動變成 Confirmed。

看到這個我反而比較安心。

因為今天只是把題目找對。

系統沒有順便幫我答題。


李逵找到了,但他說的就一定是真的嗎?

回頭看這一天。

一開始我以為 Workbook 有 Duplicate,差點想去重。

結果是 Audit 自己把資料粒度拍扁,二十個不同回傳欄位只剩同一張 (object, api) 身分證。

接著發現 Case YAML 早就留了 source.row

但它也不能因為剛好救場,就突然升格成唯一 Authority。

最後修的其實不是一個「更聰明的猜法」。

而是把它的權力範圍講清楚:

(object, api)
先圈 Candidate

source.row
只負責 Candidate 內 Tiebreak

對不上
就 Block

再把 Unicode Cf 與 Workbook Hygiene 往前搬,至少別每次都等踩雷才知道 Excel 裡住了誰。

27 個 Block 因此歸零。

很好。

但 27 個 Case 全部還在 needs_pass3

因為找到正確 Workbook Row,只能證明:

現在我們至少在回答同一題。

它不能證明 Workbook 的 Expected 是對的。

更不能證明一條 Case 連跑三次 PASS,就真的有測到它嘴上說要測的東西。

假設 Criteria 寫成:

只要 Output 不是空的
→ PASS

正確值會 PASS。

錯誤值搞不好也 PASS。

然後你跑三次:

PASS
PASS
PASS

穩。

穩得跟沒測一樣。

所以李逵現在是找到了。

下一個問題是:

這個李逵,到底會不會打李鬼?

如果我故意餵它一個錯答案,它會不會真的 FAIL?

還是它只會笑笑跟你說:

Looks good to me.

下一篇,我們不再一直餵正確答案。

換餵錯的。

看它到底會不會咬人。

Have a nice day.


上一篇
Day 13 - 415 個 Case 不可能每次從頭來:Audit 也要會中斷續跑
下一篇
Day 15 - 你說你看過 Source?先把 Tool Call 拿出來 不能再自己證明判準正確
系列文
一天一個 PR,讓我告訴你嵌入式開發的殘酷:如何為 AI Agent 建立可信的工程閉環21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言